﻿using AForge.Imaging.Filters;
using AForge.Vision.Motion;
using Clancy.Common;
using Clancy.Data;
using Clancy.Interfaces;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Reactive.Subjects;

namespace Clancy.Bl.Surveilance
{
    internal enum AlarmDelayStatus
    {
        Off,
        Waiting,
        WriteNow,
    }

    public class Surveilance : IObserver<Bitmap>, ISurveilanceStatus, IDisposable
    {
        private static readonly Brush s_BlackBrush = new SolidBrush(Color.Black);
        private static readonly Brush s_WhiteBrush = new SolidBrush(Color.White);
        private readonly Action<ISurveilanceObject, string> m_CreateNewRecordingCallback;
        private readonly BehaviorSubject<ISurveilanceResultFrame> m_CurrentProcessedFrame = new BehaviorSubject<ISurveilanceResultFrame>(null);
        private readonly Font m_Font = new Font(FontFamily.GenericSansSerif, 10);
        private readonly BehaviorSubject<bool> m_RecordingStatusSubject = new BehaviorSubject<bool>(false);
        private readonly DateTime m_StartupTime;
        private readonly ISurveilanceObject m_SurveilanceObject;

        private readonly VideoBuffer m_VideoBuffer;
        private DateTime m_AlarmDelayStartTime;
        private AlarmDelayStatus m_AlarmDelayStatus;
        private bool m_AlarmTriggered;
        private DateTime m_LastFrameTime;
        private Mirror m_Mirror;
        private MotionDetector m_MotionDetector;
        private TwoFramesDifferenceDetector m_MotionDetectorAlgorithm;
        private RotateBilinear m_Rotate;

        public Surveilance(ISurveilanceObject surveilanceObject, Action<ISurveilanceObject, string> createNewRecordingCallback)
        {
            m_StartupTime = DateTime.Now;
            m_SurveilanceObject = surveilanceObject;
            m_CreateNewRecordingCallback = createNewRecordingCallback;

            if (m_SurveilanceObject == null)
            {
                throw new ArgumentException("SurveilanceObject not of expected type");
            }

            m_SurveilanceObject.PropertyChanged += OnSurveilancePropertyChanged;

            m_VideoBuffer = new VideoBuffer(m_SurveilanceObject.PrefetchTime);

            InitializeImageProcessing();
        }

        public IObservable<ISurveilanceResultFrame> CurrentProcessedFrame
        {
            get { return m_CurrentProcessedFrame; }
        }

        public bool IsInActivationBlindPeridode
        {
            get { return ((DateTime.Now - m_StartupTime).TotalSeconds < m_SurveilanceObject.StartupBlindTime); }
        }

        public IObservable<bool> RecordingStatus
        {
            get { return m_RecordingStatusSubject; }
        }

        public ISurveilanceObject SurveilanceObject
        {
            get { return m_SurveilanceObject; }
        }

        public void Dispose()
        {
            m_SurveilanceObject.PropertyChanged -= OnSurveilancePropertyChanged;
            m_VideoBuffer.Clear();
        }

        public void OnCompleted()
        {
            throw new NotImplementedException();
        }

        public void OnError(Exception error)
        {
            throw new NotImplementedException();
        }

        public void OnNext(Bitmap value)
        {
            if (value == null)
                return;

            ProcessIncomingFrame(value.Clone(new Rectangle(0, 0, value.Width, value.Height), PixelFormat.Format24bppRgb));
        }

        private Bitmap CreateVideoTimestamp(Bitmap currentFrame, DateTime recordTime)
        {
            string timestring = recordTime.ToString("yyyy-MM-dd hh:mm:ss.fff");
            using (Graphics g = Graphics.FromImage(currentFrame))
            {
                SizeF measureString = g.MeasureString(timestring, m_Font);
                g.FillRectangle(s_BlackBrush, 0, 0, measureString.Width + 20, measureString.Height + 10);
                g.DrawString(timestring, m_Font, s_WhiteBrush, 0, 5);
            }
            return currentFrame;
        }

        private Bitmap FrameProcessing(Bitmap frame)
        {
            if (frame == null)
                return null;

            Bitmap processedFrame = frame.Clone(new Rectangle(0, 0, frame.Width, frame.Height),
                PixelFormat.Format24bppRgb);
            if (m_SurveilanceObject.RotateAngle != 0)
            {
                processedFrame = m_Rotate.Apply(processedFrame);
            }

            if (m_SurveilanceObject.FlipX || m_SurveilanceObject.FlipY)
            {
                processedFrame = m_Mirror.Apply(processedFrame);
            }
            return processedFrame;
        }

        private void InitializeImageProcessing()
        {
            m_Rotate = new RotateBilinear(m_SurveilanceObject.RotateAngle, true);
            m_Mirror = new Mirror(m_SurveilanceObject.FlipX, m_SurveilanceObject.FlipY);
            m_MotionDetectorAlgorithm = new TwoFramesDifferenceDetector(true);
            m_MotionDetector = new MotionDetector(m_MotionDetectorAlgorithm) { MotionZones = m_SurveilanceObject.MotionZones };
        }

        private void OnSurveilancePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "RecordBaseDirectory":
                    break;

                case "MotionZones":
                    m_MotionDetector.MotionZones = m_SurveilanceObject.MotionZones;
                    break;

                case "RotateAngle":
                    m_Rotate = new RotateBilinear(m_SurveilanceObject.RotateAngle, true);
                    break;

                case "FlipX":
                case "FlipY":
                    m_Mirror = new Mirror(m_SurveilanceObject.FlipX, m_SurveilanceObject.FlipY);
                    break;
            }
        }

        private void ProcessIncomingFrame(Bitmap frame)
        {
            if (frame == null)
                return;

            ISurveilanceResultFrame surveilanceResultFrame = new SurveilanceResultFrame { Timestamp = DateTime.Now };

            TimeSpan timeSinceLastFrame = surveilanceResultFrame.Timestamp - m_LastFrameTime;

            //if (timeSinceLastFrame < m_TimeBetweenFrames)
            if ((m_SurveilanceObject.Framerate > 0) &&
                (timeSinceLastFrame.TotalMilliseconds < (1000 / m_SurveilanceObject.Framerate - 1)))
            {
                return;
            }

            surveilanceResultFrame.FramesPerSecond = 1000 / timeSinceLastFrame.TotalMilliseconds;

            m_LastFrameTime = surveilanceResultFrame.Timestamp;

            DateTime recordTime = DateTime.Now;
            frame = FrameProcessing(frame);

            float frameDifference = 0;
            if (m_MotionDetector != null)
            {
                frameDifference = m_MotionDetector.ProcessFrame(frame);
                if (IsInActivationBlindPeridode)
                {
                    return;
                }

                if ((frameDifference * 100) > m_SurveilanceObject.Threshold)
                {
                    m_VideoBuffer.KeepAllFrames();

                    if (m_VideoBuffer.BufferedVideoDuration > m_SurveilanceObject.MaxRecordDuration)
                    {
                        ISurveilanceResultFrame[] framesToWrite = m_VideoBuffer.CurrentFrames.ToArray();
                        SurveilanceVideoFileWriter writer = new SurveilanceVideoFileWriter(m_SurveilanceObject, framesToWrite, m_CreateNewRecordingCallback);
                        writer.WriteVideoFile();

                        m_VideoBuffer.Clear();
                    }

                    if (m_SurveilanceObject.MotionDetectionEnabled)
                    {
                        m_AlarmTriggered = true;
                        m_AlarmDelayStatus = AlarmDelayStatus.Off;
                    }
                }
                else if (m_AlarmTriggered)
                {
                    if (m_AlarmDelayStatus == AlarmDelayStatus.Off)
                    {
                        m_AlarmDelayStartTime = DateTime.Now;
                        m_AlarmDelayStatus = AlarmDelayStatus.Waiting;
                    }
                    m_AlarmTriggered = false;
                }

                if (m_AlarmDelayStatus == AlarmDelayStatus.Waiting)
                {
                    if ((DateTime.Now - m_AlarmDelayStartTime) >= m_SurveilanceObject.AlarmDelayTime)
                    {
                        m_AlarmDelayStatus = AlarmDelayStatus.WriteNow;
                    }
                }

                if (m_AlarmDelayStatus == AlarmDelayStatus.WriteNow)
                {
                    ISurveilanceResultFrame[] framesToWrite = m_VideoBuffer.CurrentFrames;
                    SurveilanceVideoFileWriter writer = new SurveilanceVideoFileWriter(m_SurveilanceObject, framesToWrite, m_CreateNewRecordingCallback);
                    writer.WriteVideoFile();

                    m_AlarmDelayStatus = AlarmDelayStatus.Off;
                    m_VideoBuffer.ReleaseOldFrames();
                }
            }

            if (m_SurveilanceObject.RenderTimestamp)
            {
                frame = CreateVideoTimestamp(frame, recordTime);
            }

            surveilanceResultFrame.Frame = frame.ToJpegByteArray();
            surveilanceResultFrame.Timestamp = recordTime;
            surveilanceResultFrame.FrameDifference = frameDifference;
            surveilanceResultFrame.AlarmTriggered = m_AlarmTriggered;
            surveilanceResultFrame.Recording = (m_AlarmDelayStatus == AlarmDelayStatus.Waiting);

            m_VideoBuffer.Insert(surveilanceResultFrame);

            m_CurrentProcessedFrame.OnNext(surveilanceResultFrame);

            m_RecordingStatusSubject.OnNext(m_AlarmDelayStatus != AlarmDelayStatus.Off);
        }
    }
}